/*********************************
 * jsmud: JavaScript MUD Kernel  *
 *          Mojodo, Inc.         *
 *        Sean Middleditch       *
 * Copyright (C) 2005,2007,2008  *
 *      All rights reserved.     *
 *   See COPYING for licensing.  *
 *********************************/

/* ==== SYSTEM INCLUDES ==== */

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <stdint.h>
#include <stdarg.h>
#include <string.h>
#include <errno.h>
#include <sys/time.h>
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netdb.h>
#include <signal.h>
#include <arpa/telnet.h>
#include <ctype.h>
#include <limits.h>

/* ==== LIBRARY INCLUDES ==== */

#include <sqlite3.h>
#include <zlib.h>

#define XP_UNIX 1
#define MOZILLA_1_8_BRANCH 1
#include <jsapi.h>
#undef XP_UNIX
#undef MOZILLA_1_8_BRANCH

/* ==== JSMUD HEADER ==== */

#include "jsmud.h"

/* ==== GLOBALS ==== */

/* -- Network -- */
Listen* listen_list = NULL;
Client* client_list = NULL;
Client* client_next = NULL;

/* -- ZMP Handlers -- */
ZMPHandler* zmp_handler_list = NULL;

/* ==== FUNCTIONS ==== */

/* -- ZMP Handlers -- */

/* register a ZMP handler */
ZMPHandler* zmp_register (const char* name, jsval* callback)
{
	ZMPHandler* handler;

	if ((handler = (ZMPHandler*)malloc(sizeof(ZMPHandler))) == NULL)
		return NULL;
 
	handler->name = strdup(name);
	if (handler->name == NULL) {
		log_error("malloc() failed: %s", strerror(errno));
		free(handler);
		return NULL;
	}
	handler->callback = *callback;
	handler->next = zmp_handler_list;
	zmp_handler_list = handler;

	JS_AddRoot(js_context, &handler->callback);

	return handler;
}

/* invoke zmp handler */
int zmp_call (Client* client, int argc, const char** argv)
{
	ZMPHandler* handler;
	JSObject* array;
	JSFunction* callback;
	jsval js_argv[2];
	jsval retval;
	int i;
	const char* command = argv[0];

	/* must be at least one argument, the command name */
	if (argc < 1)
		return 0;

	/* find handler */
	for (handler = zmp_handler_list; handler != NULL; handler = handler->next) {
		if (!strcasecmp(command, handler->name))
			break;
	}
	if (handler == NULL)
		return 0;

	/* get callback */
	if ((callback = JS_ValueToFunction(js_context, handler->callback)) == NULL)
		return 0;

	/* generate call array */
	if ((array = JS_NewArrayObject(js_context, 0, NULL)) == NULL) {
		log_error("JS_NewArrayObject() failed: %s", strerror(errno));
		return 0;
	}
	js_argv[0] = OBJECT_TO_JSVAL(client->object);
	js_argv[1] = OBJECT_TO_JSVAL(array);
	JS_AddRoot(js_context, &js_argv[1]);
	for (i = 0; i < argc; ++i) {
		if (JS_DefineElement(js_context, array, i, STRING_TO_JSVAL(JS_NewStringCopyZ(js_context, argv[i])), NULL, NULL, 0) == JS_FALSE) {
			JS_RemoveRoot(js_context, &js_argv[1]);
			log_error("JS_DefineElement() failed: %s", strerror(errno));
			return 0;
		}
	}

	/* invoke handler */
	JS_CallFunction(js_context, js_global, callback, 2, js_argv, &retval);

	/* unroot argv */
	JS_RemoveRoot(js_context, &js_argv[1]);

	return 1;
}

/*  check if we support a given command or package */
int zmp_has (const char* name)
{
	int length;
	ZMPHandler* handler;

	/* empty name?  not valid, "we don't have it" */
	length = strlen(name);
	if (length == 0)
		return 0;

	/* is it a package? */
	if (name[length - 1] == '.') {
		for (handler = zmp_handler_list; handler != NULL; handler = handler->next) {
			if (!strncasecmp(name, handler->name, length))
				return 1;
		}
	/* it's a command */
	} else {
		for (handler = zmp_handler_list; handler != NULL; handler = handler->next) {
			if (!strcasecmp(name, handler->name))
				return 1;
		}
	}

	/* didn't find it */
	return 0;
}

void zmp_free_all (void)
{
	ZMPHandler* next;
	while (zmp_handler_list != NULL) {
		free(zmp_handler_list->name);
		JS_RemoveRoot(js_context, &zmp_handler_list->callback);

		next = zmp_handler_list->next;
		free(zmp_handler_list);
		zmp_handler_list = next;
	}
}

/* -- Networking -- */

Listen* listen_new (const char* interface, short port, jsval* callback)
{
	Listen* listp;
	struct addrinfo hints;
	struct addrinfo* res;
	int sock;
	int error;
	int opt;

	/* lookup address for interface */
	memset(&hints, 0, sizeof(hints));
	hints.ai_flags = AI_PASSIVE;
	hints.ai_socktype = SOCK_STREAM;

	if ((error = getaddrinfo(interface, NULL, &hints, &res)) != 0) {
		log_error("getaddrinfo() failed: %s", gai_strerror(error));
		return NULL;
	}

	/* setup the socket for listening */
	if ((sock = socket(res->ai_family, res->ai_socktype, res->ai_protocol)) == -1) {
		log_error("socket() failed: %s", strerror(errno));
		freeaddrinfo(res);
		return NULL;
	}

	opt = 1;
	setsockopt (sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

	((struct sockaddr_in*)res->ai_addr)->sin_port = htons(port);
	if (bind(sock, res->ai_addr, res->ai_addrlen) == -1) {
		log_error("bind() failed: %s", strerror(errno));
		close(sock);
		freeaddrinfo(res);
		return NULL;
	}

	freeaddrinfo(res);

	if (listen(sock, 5) == -1) {
		log_error("listen() failed: %s", strerror(errno));
		close(sock);
		return NULL;
	}

	/* build Listen object */
	if ((listp = (Listen*)malloc(sizeof(Listen))) == NULL) {
		close(sock);
		return NULL;
	}
	if ((listp->interface = strdup(interface)) == NULL) {
		free(listp);
		close(sock);
		return NULL;
	}
	listp->port = port;
	listp->sock = sock;
	listp->next = listen_list;
	listp->callback = *callback;
	listen_list = listp;

	/* root the function */
	JS_AddRoot(js_context, &listp->callback);

	/* log what we did */
	log_msg("Listening on %s.%d", interface, port);

	return listp;
}

void listen_accept (fd_set* set)
{
	Listen* listen;
	Client* client;
	int sock;
	jsval rval;

	for (listen = listen_list; listen != NULL; listen = listen->next) {
		/* if the listener socket is set, accept a connection */
		if (FD_ISSET(listen->sock, set)) {
			/* error; connection aborted */
			if ((sock = accept(listen->sock, NULL, NULL)) == -1)
				continue;

			/* log connection */
			log_msg("Client connected to %s.%d", listen->interface, listen->port);
			
			/* create client object */
			client = client_new(sock);
			if (client == NULL) {
				close(sock);
				continue;
			}

			/* invoke accept function */
			JS_CallFunction(js_context, js_global, JS_ValueToFunction(js_context, listen->callback), 1, &client->object, &rval);
		}
	}
}

void build_fd_set (fd_set* set, int* max)
{
	Client* client;
	Listen* listen;

	FD_ZERO(set);
	*max = -1;

	for (listen = listen_list; listen != NULL; listen = listen->next) {
		FD_SET(listen->sock, set);
		if (listen->sock > *max)
			*max = listen->sock;
	}

	for (client = client_list; client != NULL; client = client->next) {
		FD_SET(client->sock, set);
		if (client->sock > *max)
			*max = client->sock;
	}
}

Client* client_new (int sock)
{
	Client* client;
	JSObject* object;

	if ((client = (Client*)malloc(sizeof(Client))) == NULL)
		return NULL;

	/* allocate command buffer */
	client->cmd_len = 0;
	client->cmd_buf = (char*)malloc(TELNET_COMMAND_SIZE);
	if (client->cmd_buf == NULL) {
		free(client);
		return NULL;
	}

	/* create JavaScript object */
	if ((object = JS_NewObject(js_context, &js_connection_class, js_connection_prototype, NULL)) == NULL) {
		free(client);
		return NULL;
	}
	client->object = OBJECT_TO_JSVAL(object);
	JS_AddRoot(js_context, &client->object);
	JS_SetPrivate(js_context, object, client);

	/* initialize */
	client->tmode = TM_TEXT;
	client->imode = IN_LINE;
	client->zlib = NULL;
	client->sub_buf = NULL;
	client->sub_len = 0;
	client->sock = sock;
	client->flags = TF_WANT_ECHO;
	
	/* add to list */
	client->prev = NULL;
	client->next = client_list;
	client_list = client;
	if (client->next)
		client->next->prev = client;
	
	/* telnet option advertising */
	client_send_iac(client, WILL, TELOPT_ZMP);
	client_send_iac(client, WILL, TELOPT_MCCP);

	return client;
}

void client_send_raw (Client* client, const char* bytes, int len)
{
	int err;

	if (client->sock == -1)
		return;

	if (len == 0)
		return;

	while (len > 0) {
		err = write(client->sock, bytes, len);
		if (err == -1 && errno != EAGAIN) {
			log_error("write() failed: %s", strerror(errno));
			client_disconnect(client);
			return;
		}

		if (err > 0) {
			bytes += err;
			len -= err;
		}
	}
}

void client_send (Client* client, const char* bytes, int len)
{
	char output[2048];

	/* no MCCP - call real send */
	if (client->zlib == NULL) {
		client_send_raw(client, bytes, len);
		return;
	}
		
	/* deflate loop and send results */
	client->zlib->next_in = (unsigned char*)bytes;
	client->zlib->avail_in = len;
	do {
		client->zlib->next_out = (unsigned char*)output;
		client->zlib->avail_out = sizeof(output);
		if (deflate(client->zlib, Z_NO_FLUSH) != Z_OK)
			break;
		client_send_raw(client, output, sizeof(output) - client->zlib->avail_out);
	} while (client->zlib->avail_out < sizeof(output) || client->zlib->avail_in > 0);
}

void client_send_text (Client* client, const char* bytes, int length)
{
	char esc[2] = { IAC, IAC };
	char nl[2] = { '\n', '\r' };
	int i, last;

	if (length == -1)
		length = strlen(bytes);

	last = 0;
	for (i = 0; i < length; ++i) {
		if ((unsigned char)bytes[i] == IAC) {
			client_send(client, bytes + last, i - last);
			client_send(client, esc, sizeof(esc));
			last = i + 1;
		} else if (bytes[i] == '\n') {
			client_send(client, bytes + last, i - last);
			client_send(client, nl, sizeof(nl));
			last = i + 1;
		}
	}
	if (last != length) 
		client_send(client, bytes + last, length - last);
}

void client_send_data (Client* client, const char* bytes, int length)
{
	char esc[2] = { IAC, IAC };
	int i, last;

	if (length == -1)
		length = strlen(bytes);

	last = 0;
	for (i = 0; i < length; ++i) {
		if ((unsigned char)bytes[i] == IAC) {
			client_send(client, bytes + last, i - last);
			client_send(client, esc, sizeof(esc));
			last = i + 1;
		}
	}
	if (last != length) 
		client_send(client, bytes + last, length - last);
}

void client_send_iac (Client* client, int cmd, int opt)
{
	char iac[3] = { IAC, cmd, opt };
	client_send(client, iac, sizeof(iac));
}

void client_send_zmp (Client* client, const char* command, int argc, const char** argv)
{
	int i;
	char begin[4] = { IAC, SB, TELOPT_ZMP };
	char end[2] = { IAC, SE };

	client_send(client, begin, sizeof(begin));

	/* send command, including NUL byte */
	client_send_data(client, command, strlen(command) + 1);

	/* send args, including trailing NUL bytes */
	for (i = 0; i < argc; ++i)
		client_send_data(client, argv[i], strlen(argv[i]) + 1);

	client_send(client, end, sizeof(end));
}

void client_disconnect (Client* client)
{
	log_msg("Disconnecting client %d", client->sock);

	/* unconnect JavaScript object */
	JS_SetPrivate(js_context, jsval_to_object(client->object), NULL);
	JS_RemoveRoot(js_context, &client->object);
	client->object = JSVAL_NULL;

	/* fixup the linked list */
	if (client->next)
		client->next->prev = client->prev;
	if (client->prev)
		client->prev->next = client->next;
	else
		client_list = client->next;

	/* free memory */
	free(client->cmd_buf);
	if (client->sub_buf)
		free(client->sub_buf);

	/* shutdown the socket */
	close(client->sock);

	/* if we were next, move the pointer along */
	if (client_next == client)
		client_next = client->next;

	/* free memory */
	free(client);
}

void client_update (fd_set* set)
{
	Client* client;
	int err;
	char buffer[1024 * 4];

	client_next = NULL;
	for (client = client_list; client != NULL; client = client_next) {
		/* the next client needs to be preset */
		client_next = client->next;

		/* if the client socket is set, read input */
		if (FD_ISSET(client->sock, set)) {
			/* read, handle error */
			err = read(client->sock, buffer, sizeof(buffer));
			if (err == -1) {
				if (errno != EAGAIN) {
					client_disconnect(client);
					log_error("read() failed: %s", strerror(errno));
				}
				continue;
			/* EOF */
			} else if (err == 0) {
				client_disconnect(client);

				js_call_method(jsval_to_object(client->object), "onHangup", 0, NULL);
			/* data */
			} else {
				client_process(client, buffer, err);
			}
		}
	}
}

void client_flush (void)
{
	Client* client;
	JSObject* object;
	char output[2048];

	for (client = client_list; client != NULL; client = client->next) {
		/* tell client object we're about to flush */
		JS_ValueToObject(js_context, client->object, &object);
		js_call_method(object, "onFlush", 0, NULL);

		/* flush any zlib buffer cruft */
		if (client->zlib != NULL) {
			client->zlib->next_in = NULL;
			client->zlib->avail_in = 0;
			do {
				client->zlib->next_out = (unsigned char*)output;
				client->zlib->avail_out = sizeof(output);
				if (deflate(client->zlib, Z_SYNC_FLUSH) != Z_OK)
					break;
				client_send_raw(client, output, sizeof(output) - client->zlib->avail_out);
			} while (client->zlib->avail_out != sizeof(output));
		}
	}
}

void client_process (Client* client, const char* bytes, int length)
{
	unsigned char c;
	int i;
	jsval arg;
	JSObject* object;
	char begin_mccp[5] = { IAC, SB, TELOPT_MCCP, IAC, SE };

	/* get client object for script callbacks */
	JS_ValueToObject(js_context, client->object, &object);

	/* process all bytes */
	for (i = 0; i < length; ++i) {
		c = (unsigned char)bytes[i];

		/* alter interpretation of byte based on mode */
		switch (client->tmode) {
			/* user text */
			case TM_TEXT:
				/* if it's an IAC, go into escape mode */
				if (c == IAC) {
					client->tmode = TM_IAC;
					break;
				}

				/* handle based on mode */
				switch (client->imode) {
					/* character mode - send to scripts */
					case IN_CHARACTER:
						arg = INT_TO_JSVAL(c);
						js_call_method(object, "onInput", 1, &arg);
						break;
					/* line mode: buffer line, send to script on newline */
					case IN_LINE:
						/* send to script */
						if (c == '\n') {
							arg = STRING_TO_JSVAL(JS_NewStringCopyN(js_context, client->cmd_buf, client->cmd_len));
							js_call_method(object, "onInput", 1, &arg);
							client->cmd_len = 0;
						/* just buffer */
						} else {
							client_append_text(client, c);
						}
						break;
					/* null mode - ignore, or return to line mode on newline */
					case IN_NULL:
						if (c == '\n')
							client->imode = IN_LINE;
						break;
				}

				/* echo if necessary */
				if (client->flags & TF_DO_ECHO && client->flags & TF_WANT_ECHO)
					client_send_data(client, (char*)&c, 1);
				break;
			/* IAC sequence */
			case TM_IAC:
				/* process byte code */
				switch (c) {
					/* IAC escape - add IAC byte to text */
					case IAC:
						client_append_text(client, IAC);
						client->tmode = TM_TEXT;

						/* echo if necessary */
						if (client->flags & TF_DO_ECHO && client->flags & TF_WANT_ECHO)
							client_send_data(client, (char*)&c, 1);
						break;
					/* DO/DONT/WILL/WONT commands */
					case WILL:
						client->tmode = TM_WILL;
						break;
					case WONT:
						client->tmode = TM_WONT;
						break;
					case DO:
						client->tmode = TM_DO;
						break;
					case DONT:
						client->tmode = TM_DONT;
						break;
					/* sub request */
					case SB:
						client->tmode = TM_SUB;
						client->sub_len = 0;
						client->sub_buf = (char*)malloc(TELNET_SUB_REQUEST_SIZE);
						if (client->sub_buf == NULL)
							log_error("malloc() failed: %s", strerror(errno));
						break;
					/* unknown - two-code sequence, ignore */
					default:
						client->tmode = TM_TEXT;
						break;
				}
				break;
			/* WILL command */
			case TM_WILL:
				client->tmode = TM_TEXT;
				break;
			/* WONT command */
			case TM_WONT:
				client->tmode = TM_TEXT;
				break;
			/* DO command */
			case TM_DO:
				switch (c) {
					/* asking us to echo */
					case TELOPT_ECHO:
						client->flags |= TF_DO_ECHO;
						break;
					/* enable ZMP */
					case TELOPT_ZMP:
						if ((client->flags & TF_HAVE_ZMP) == 0) {
							client->flags |= TF_HAVE_ZMP;
							js_call_method(jsval_to_object(client->object), "enableZMP", 0, NULL);
						}
						break;
					/* use MCCP */
					case TELOPT_MCCP:
						if (client->zlib == NULL) {
							if ((client->zlib = (struct z_stream_s*)malloc(sizeof(struct z_stream_s))) == NULL) {
								log_error("malloc() failed: %s", strerror(errno));
								break;
							}
							memset(client->zlib, 0, sizeof(struct z_stream_s));
							if (deflateInit(client->zlib, Z_DEFAULT_COMPRESSION) != Z_OK) {
								log_error("deflateInit() failed: %s", client->zlib->msg);
								free(client->zlib);
								client->zlib = NULL;
								break;
							}
							client_send_raw(client, begin_mccp, sizeof(begin_mccp));
						}
						break;
					/* default response: refuse to do whatever we were just told to do */
					default:
						client_send_iac(client, WONT, c);
						break;
				}
				client->tmode = TM_TEXT;
				break;
			/* DONT command */
			case TM_DONT:
				switch (c) {
					/* asking us not to echo */
					case TELOPT_ECHO:
						client->flags &= ~TF_DO_ECHO;
						break;
					/* telling us not use some option; just ignore it quietly */
					case TELOPT_ZMP:
					case TELOPT_MCCP:
						break;
					/* default response: refuse to do whatever we were just told to do */
					default:
						client_send_iac(client, WONT, c);
						break;
				}
				client->tmode = TM_TEXT;
				break;
			/* sub-request */
			case TM_SUB:
				/* if its an IAC, go into escape mode */
				if (c == IAC) {
					client->tmode = TM_SUB_IAC;
				/* only process if we have a sub buffer */
				} else if (client->sub_buf) {
					/* if we're full, error out of the sub-request */
					if (client->sub_len == TELNET_SUB_REQUEST_SIZE) {
						free(client->sub_buf);
						client->sub_buf = NULL;
						/* FIXME: log error, respond, etc? */
					/* not full, add byte to buffer */
					} else {
						client->sub_buf[client->sub_len++] = c;
					}
				}
				break;
			/* sub-request IAC */
			case TM_SUB_IAC:
				/* if it's SE, end sub-request and process */
				if (c == SE) {
					if (client->sub_buf) {
						client_handle_telnet_subreq(client);
						free(client->sub_buf);
						client->sub_buf = NULL;
					}
					client->tmode = TM_TEXT;
				/* if it's IAC, add IAC byte to sr buffer */
				} else if (c == IAC) {
					client->tmode = TM_IAC;
					/* only append if we have a buffer */
					if (client->sub_buf) {
						/* if we're full, error out of the sub-request */
						if (client->sub_len == TELNET_SUB_REQUEST_SIZE) {
							free(client->sub_buf);
							client->sub_buf = NULL;
							/* FIXME: log error, respond, etc? */
						/* not full, add byte to buffer */
						} else {
							client->sub_buf[client->sub_len++] = c;
						}
					}
				/* something else; erroneous, just ignore */
				} else {
					client->tmode = TM_IAC;
				}
				break;
		}
	}
}

void client_handle_telnet_subreq (Client* client)
{
	char command;	
	const char* bytes;
	int length;

	/* must have at least one byte, the command */
	if (client->sub_len < 1) {
		/* FIXME: handle/log error in some way? */
		return;
	}

	/* extra information */
	command = client->sub_buf[0];
	bytes = client->sub_buf + 1;
	length = client->sub_len - 1;

	/* handle */
	switch (command) {
		case TELOPT_ZMP:
			client_handle_zmp (client, bytes, length);
			break;
		default:
			/* unknown */
			break;
	}
}

void client_handle_zmp (Client* client, const char* bytes, int length)
{
	int argc;
	const char* argv[TELNET_ZMP_MAX_ARGS];

	/* ZMP requires at least two bytes of data in payload */
	if (length < 2)
		return;
	/* last byte must be NUL */
	if (bytes[length-1] != 0)
		return;
	/* first byte must be alphanumeric ASCII */
	if (!isalnum(bytes[0]))
		return;

	/* break into arguments */
	argc = 0;
	while (length > 0 && argc < TELNET_ZMP_MAX_ARGS) {
		argv[argc++] = bytes;

		length -= strlen(bytes) + 1;
		bytes += strlen(bytes) + 1;
	}

	/* if we have data left over, too many arguments */
	if (length > 0)
		return;

	/* process ZMP command */
	zmp_call(client, argc, argv);
}

int client_append_text (Client* client, char c)
{
	/* only bother with printing characters */
	if (!isprint(c) || c == '\r')
		return 1;

	/* check size */
	if (client->cmd_len == TELNET_COMMAND_SIZE) {
		client->imode = IN_NULL;
		client_send_text(client, "\n<< Command too long, command ignored >>\n", -1);
		client->cmd_len = 0;
		return 0;
	}

	/* append character */
	client->cmd_buf[client->cmd_len++] = c;

	return 1;
}

void listen_free_all (void)
{
	Listen* next;
	while (listen_list != NULL) {
		close(listen_list->sock);
		JS_RemoveRoot(js_context, &listen_list->callback);
		free(listen_list->interface);

		next = listen_list->next;
		free(listen_list);
		listen_list = next;
	}
}

void client_free_all (void)
{
	while (client_list != NULL)
		client_disconnect(client_list);
}

/* vim: set tabstop=2 shiftwidth=2 ai: */
